if funnyCubesLoaded then return end

freeslot("MT_CUBESPAWNER", "MT_CUBE", "MT_CUBEPIECE", "S_CUBEPIECE2")

local NO_SP_DEF = 5 * TICRATE
local FORCE_OUT_MOM = 32 << FRACBITS
local BUMP_FACTOR = FRACUNIT >> 1
local MIN_BUMP_MOM = 12 << FRACBITS
local BUMP_FACTOR_V = FRACUNIT >> 1
local MIN_BUMP_MOM_V = 8 << FRACBITS
local OUST_BUFFER = 4 << FRACBITS
local BOUNCE_NUDGE_LIM = 2 << FRACBITS

local function setCubeSpawnerIntervalFromSp(cubeSpawner)
    cubeSpawner.interval = cubeSpawner.spawnpoint.angle / 360 + 1
    if cubeSpawner.interval < 0 then
        cubeSpawner.interval = $ + FRACUNIT
    end
    if cubeSpawner.spawnpoint.options & 1 then
        cubeSpawner.interval = $ * TICRATE
    end
end

local function handleHorizAdjust(other, cube, localPos, relAngle)
    local forceOut =
        abs(localPos.x - cube.x) <= cube.realRadius
        and abs(localPos.y - cube.y) <= cube.realRadius
    local localPushAngle = (relAngle + ANGLE_45) / ANGLE_90 * ANGLE_90
    if relAngle + ANGLE_45 < 0 then
        localPushAngle = $ - ANGLE_90
    end
    local pushAngle = localPushAngle + cube.angle
    
    --where do we want the object to go in local space?
    local localTarget = {x = localPos.x, y = localPos.y}
    if localPushAngle == 0 then
        localTarget.x = cube.x + cube.realRadius + other.radius + OUST_BUFFER
    elseif localPushAngle == ANGLE_90 then
        localTarget.y = cube.y + cube.realRadius + other.radius + OUST_BUFFER
    elseif localPushAngle == ANGLE_180 then
        localTarget.x = cube.x - cube.realRadius - other.radius - OUST_BUFFER
    else
        localTarget.y = cube.y - cube.realRadius - other.radius - OUST_BUFFER
    end
    
    --get the distance for rotation
    local targetDist = R_PointToDist2(cube.x, cube.y, localTarget.x, localTarget.y)
    --get angle for offset from cube's angle
    local targetAngleOffset = R_PointToAngle2(cube.x, cube.y, localTarget.x, localTarget.y)
    
    P_TeleportMove(
        other,
        cube.x + FixedMul(cos(cube.angle + targetAngleOffset), targetDist),
        cube.y + FixedMul(sin(cube.angle + targetAngleOffset), targetDist),
        other.z
    )
    local otherThrust = P_AproxDistance(other.momx, other.momy)
    if forceOut then
        P_InstaThrust(other, pushAngle, max(otherThrust, FORCE_OUT_MOM))
    else
        --angle comparison overflow management at 6 a.m. is great
        local thrustAngle = R_PointToAngle2(0, 0, other.momx, other.momy)
        local passL = thrustAngle < pushAngle - ANGLE_90
        local passH = thrustAngle > pushAngle + ANGLE_90
        local doMirror = false
        if pushAngle > ANGLE_270 and pushAngle < ANGLE_90 then
            if passL or passH then
                doMirror = true
            end
        elseif passL and passH then
            doMirror = true
        end
        
        if doMirror then
            otherThrust = FixedMul(P_AproxDistance(other.momx, other.momy), BUMP_FACTOR)
            --reflect according to push angle's local space
            P_InstaThrust(
                other,
                InvAngle(thrustAngle - pushAngle + ANGLE_90) + pushAngle - ANGLE_90,
                otherThrust
            )
        end
        
        --the cube push has bias in the cube's current mom
        --unfortunately, this doesn't seem to account conveyors. this is so sad
        local cubeThrust = P_AproxDistance(cube.momx, cube.momy)
        local cubeThrustAngle = R_PointToAngle2(0, 0, cube.momx, cube.momy)
        local bumpcodeAngle = pushAngle - cubeThrustAngle
        if bumpcodeAngle > ANGLE_270 and bumpcodeAngle < ANGLE_90 then
            P_Thrust(other, cubeThrustAngle, FixedMul(cos(bumpcodeAngle), cubeThrust))
        end
        
        --additional bump if a player is not going very fast (you know, like kart bumpcode)
        if other.type == MT_PLAYER then
            --refresh momentum var
            local otherThrust = P_AproxDistance(other.momx, other.momy)
            if otherThrust < MIN_BUMP_MOM then
                P_Thrust(other, pushAngle, MIN_BUMP_MOM - otherThrust)
            end
        end
    end
end

local function handleVertAdjust(other, cube, nudgeAngle, flipped)
    --get it out
    if flipped then
        P_TeleportMove(other, other.x, other.y, cube.z - other.height + OUST_BUFFER)
    else
        P_TeleportMove(other, other.x, other.y, cube.z + cube.height + OUST_BUFFER)
    end
    --nudge them off if they're likely to get stuck
    if P_AproxDistance(other.momx, other.momy) < BOUNCE_NUDGE_LIM then
        P_Thrust(other, nudgeAngle, BOUNCE_NUDGE_LIM)
    end
    other.momz = max(FixedMul(-$, BUMP_FACTOR_V), MIN_BUMP_MOM_V)
end

local function shoveFromCube(cube, other)
    --make sure other is tangible, isn't scenery (orbiting items), and within z bounds
    if
        not (other.flags & (MF_NOCLIP | MF_NOCLIPTHING | MF_SCENERY))
        and other.z + other.height >= cube.z
        and other.z <= cube.z + cube.height
    then
        --treat collisions within the local space
        local dist = R_PointToDist2(cube.x, cube.y, other.x, other.y)
        local angleOff = R_PointToAngle2(cube.x, cube.y, other.x, other.y)
        local localAngle = angleOff - cube.angle
        local localPos = {
            x = cube.x + FixedMul(cos(localAngle), dist),
            y = cube.y + FixedMul(sin(localAngle), dist)
        }
        --how convenient; I don't need to do actual rotation math here
        --the position in local space respects the cube as a square
        if
            localPos.x >= cube.x - cube.realRadius - other.radius
            and localPos.x <= cube.x + cube.realRadius + other.radius
            and localPos.y >= cube.y - cube.realRadius - other.radius
            and localPos.y <= cube.y + cube.realRadius + other.radius
        then
            local azimuth = R_PointToAngle2(
                0,
                cube.z + cube.height / 2,
                dist,
                other.z + other.height / 2
            )
            
            if azimuth > ANGLE_45 and azimuth < ANGLE_135 then
                --hit top face
                if other.eflags & MFE_VERTICALFLIP then
                    if P_IsObjectOnGround(other) then
                        --falling on player upside-down
                        handleHorizAdjust(other, cube, localPos, localAngle)
                    end
                elseif other.momz < 0 then
                    --smacking from above
                    handleVertAdjust(other, cube, angleOff, false)
                end
            elseif azimuth > ANGLE_225 and azimuth < ANGLE_315
                --hit bottom face
                if not (other.eflags & MFE_VERTICALFLIP) then
                    if P_IsObjectOnGround(other) then
                        --falling on player
                        handleHorizAdjust(other, cube, localPos, localAngle)
                    end
                elseif other.momz > 0 then
                    --smacking from underneath
                    handleVertAdjust(other, cube, angleOff, true)
                end
            else
                --hit a side
                handleHorizAdjust(other, cube, localPos, localAngle)
            end
            S_StartSound(other, sfx_s3k49)
            local bump = P_SpawnMobj(
                other.x / 2 + cube.x / 2,
                other.y / 2 + cube.y / 2,
                other.z / 2 + cube.z / 2,
                MT_BUMP
            )
            if (other.eflags & MFE_VERTICALFLIP) then
                bump.eflags = $ | MFE_VERTICALFLIP
            else
                bump.eflags = $ & ~MFE_VERTICALFLIP
            end
            P_SetScale(bump, other.scale)
            return true
        else
            return false
        end
    else
        return false
    end
end

addHook(
    "MobjSpawn",
    function(cubeSpawner)
        if cubeSpawner.spawnpoint and cubeSpawner.spawnpoint.valid then
            setCubeSpawnerIntervalFromSp(cubeSpawner)
        else
            cubeSpawner.interval = NO_SP_DEF
        end
    end,
    MT_CUBESPAWNER
)

addHook(
    "MobjThinker",
    function(cubeSpawner)
        if cubeSpawner.spawnpoint and cubeSpawner.spawnpoint.valid then
            setCubeSpawnerIntervalFromSp(cubeSpawner)
        end
        if leveltime > 0 and not (leveltime % cubeSpawner.interval) then
            local cube = P_SpawnMobj(cubeSpawner.x, cubeSpawner.y, cubeSpawner.z, MT_CUBE)
            cube.angle = cubeSpawner.angle
        end
    end,
    MT_CUBESPAWNER
)

addHook(
    "MobjSpawn",
    function(cube)
        cube.fuse = 60 * TICRATE
        cube.realRadius = FixedDiv(cube.radius, FixedSqrt(2 << FRACBITS))
        cube.pieces = {}
        for i = 1, 8 do
            table.insert(cube.pieces, P_SpawnMobj(cube.x, cube.y, cube.z, MT_CUBEPIECE))
        end
        for i = 5, 8 do
            cube.pieces[i].state = S_CUBEPIECE2
        end
    end,
    MT_CUBE
)

addHook(
    "MobjThinker",
    function(cube)
        if P_CheckDeathPitCollide(cube)
            P_RemoveMobj(cube)
        else
            for i = 1, 8 do
                if not cube.pieces[i].valid then
                    cube.pieces[i] = P_SpawnMobj(cube.x, cube.y, cube.z, MT_CUBEPIECE)
                    if i >= 5 then
                        cube.pieces[i].state = S_CUBEPIECE2
                    end
                end
                cube.pieces[i].angle = cube.angle + ANGLE_90 * (i - 1)
                cube.pieces[i].momx = cube.momx
                cube.pieces[i].momy = cube.momy
                cube.pieces[i].momz = cube.momz
            end
            
            local positions = {}
            for i = 1, 4 do
                local x = cos(cube.pieces[i].angle + ANGLE_90)
                local y = sin(cube.pieces[i].angle + ANGLE_90)
                P_TeleportMove(
                    cube.pieces[i],
                    cube.x + FixedMul(cube.realRadius, x),
                    cube.y + FixedMul(cube.realRadius, y),
                    cube.z
                )
                P_TeleportMove(
                    cube.pieces[i + 4],
                    cube.x + FixedMul(cube.realRadius - FRACUNIT, x),
                    cube.y + FixedMul(cube.realRadius - FRACUNIT, y),
                    cube.z
                )
            end
        end
    end,
    MT_CUBE
)

addHook("MobjCollide", shoveFromCube, MT_CUBE)
addHook("MobjMoveCollide", shoveFromCube, MT_CUBE)

addHook(
    "MobjRemoved",
    function(cube)
        for i = 1, 8 do
            if cube.pieces[i].valid then
                P_RemoveMobj(cube.pieces[i])
            end
        end
    end,
    MT_CUBE
)

rawset(_G, "funnyCubesLoaded", true)

--testing stuff comment this out in final you dumbo
--[[
addHook(
    "ThinkFrame",
    function()
        if leveltime == 1 then
            local spawned = P_SpawnMobj(players[0].mo.x + (96 << FRACBITS), players[0].mo.y, players[0].mo.z + (512 << FRACBITS), MT_CUBE)
            spawned.angle = ANGLE_112h
        end
    end
)
]]